package jmine.tec.persist.schema.impl.elements;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import jmine.tec.persist.annotation.Index;
import jmine.tec.persist.schema.api.AbstractProcessingSchemaElement;
import jmine.tec.persist.schema.api.IndexOrder;
import jmine.tec.persist.schema.api.ProcessingSchemaElement;
import jmine.tec.persist.schema.api.SqlDialect;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.hibernate.mapping.Column;

/**
 * Bean representando um index
 * 
 * @author lundberg
 */
public class IndexElement extends AbstractProcessingSchemaElement<TableElement> implements Comparable<IndexElement> {

    private String name;

    private String table;

    private String tablespace;

    private List<Tuple<String, IndexOrder>> columns;

    private Set<String> columnSet;

    /**
     * Construtor
     * 
     * @param table tabela à qual se aplica o índice
     * @param name nome do índice
     * @param parent parent
     */
    public IndexElement(String table, String name, ProcessingSchemaElement<?> parent) {
        super(parent);
        this.name = name;
        this.table = table;
        this.columns = new LinkedList<Tuple<String, IndexOrder>>();
        this.columnSet = new HashSet<String>();
    }

    /**
     * Construtor
     * 
     * @param table tabela à qual se aplica o índice
     * @param index anotação
     * @param parent parent
     */
    public IndexElement(TableElement table, Index index, ProcessingSchemaElement<?> parent) {
        this(table, (String[]) null, index, parent);
    }

    /**
     * Construtor
     * 
     * @param table tabela à qual se aplica o índice
     * @param columnIterator iterador de elementos do tipo org.hibernate.mapping.Column, usado caso o índice não defina as colunas
     * explicitamente
     * @param index anotação
     * @param parent parent
     */
    @SuppressWarnings("deprecation")
    public IndexElement(TableElement table, Iterator<?> columnIterator, Index index, ProcessingSchemaElement<?> parent) {
        this(table.getName(), (String) null, parent);
        this.retrieveNameFromAnnotation(table, index);
        String[] cols;
        if (index.columns().length == 0) {
            List<String> colsList = new LinkedList<String>();
            while (columnIterator.hasNext()) {
                Column column = (Column) columnIterator.next();
                colsList.add(column.getName());
            }
            cols = colsList.toArray(new String[0]);
        } else {
            cols = index.columns();
        }
        this.tablespace = index.tablespace();
        this.addColumns(cols, index.orders(), index.asc() ? IndexOrder.ASC : IndexOrder.UNDEFINED);
    }

    /**
     * Extrai o nome a partir da anotação.
     * 
     * @param tableElement table
     * @param index index
     */
    private void retrieveNameFromAnnotation(TableElement tableElement, Index index) {
        if (StringUtils.isNotBlank(index.suffix())) {
            this.name = String.format("IX_%s_%s", tableElement.getAlias(), index.suffix());
        } else if (StringUtils.isNotBlank(index.name())) {
            this.name = index.name();
        }
    }

    /**
     * Construtor
     * 
     * @param table tabela à qual se aplica o índice
     * @param columns colunas que devem ser indexadas, caso o índice não defina explicitamente
     * @param index anotação
     * @param parent parent
     */
    @SuppressWarnings("deprecation")
    public IndexElement(TableElement table, String[] columns, Index index, ProcessingSchemaElement<?> parent) {
        this(table.getName(), (String) null, parent);
        this.retrieveNameFromAnnotation(table, index);
        String[] cols = index.columns();
        this.tablespace = index.tablespace();
        this.addColumns(cols, index.orders(), index.asc() ? IndexOrder.ASC : IndexOrder.UNDEFINED);
    }

    /**
     * {@inheritDoc}
     */
    public List<String> createStatements(SqlDialect dialect) {
        return dialect.renderIndex(this.tablespace, this.table, this.name, this.columns);
    }

    /**
     * {@inheritDoc}
     */
    public void process(TableElement tableElement) {

        this.table = tableElement.getName();
        if (this.name == null) {
            String alias = tableElement.getAlias();
            Set<String> existing = new HashSet<String>();
            for (IndexElement index : tableElement.getIndexes()) {
                existing.add(index.name);
            }
            int i = 0;
            while (existing.contains(String.format("IX_%s_%s", alias, i))) {
                i++;
            }
            this.name = String.format("IX_%s_%s", alias, i);
            List<String> otherIndexNames = new ArrayList<String>();
            for (IndexElement index : tableElement.getIndexes()) {
                if (index.name != null) {
                    otherIndexNames.add(index.name);
                }
            }
            this.warn(String.format(
                    "Indice '%s' gerado automaticamente na tabela '%s' nas colunas %s, favor definir o nome. Outros índices na tabela: %s",
                    this.name, this.table, this.columns, StringUtils.join(otherIndexNames, ", ")));
        } else {
            if (!this.name.startsWith(String.format("IX_%s_", tableElement.getAlias()))) {
                this.warn(String.format("Nome de índice definido fora do padrão '%s' na tabela '%s', o correto é 'IX_%s'", this.name,
                        this.table, tableElement.getAlias()));
            }
        }
    }

    /**
     * Adiciona o array de colunas na ordem dada.
     * 
     * @param cols cols
     * @param orders orders
     * @param def valor padrão para a ordem
     */
    private void addColumns(String[] cols, IndexOrder[] orders, IndexOrder def) {
        for (int i = 0; i < cols.length; i++) {
            String column = cols[i];
            IndexOrder order = orders.length > i ? orders[i] : def;
            this.addColumn(column, order);
        }
    }

    /**
     * Adiciona a coluna ao índice com a ordenação dada.
     * 
     * @param column column
     * @param order order
     */
    public void addColumn(String column, IndexOrder order) {
        this.columns.add(new UnmodifiableTuple<String, IndexOrder>(column, order));
        this.columnSet.add(column);
    }

    /**
     * Adiciona a coluna ao índice com ordem não definida.
     * 
     * @param column column
     */
    public void addColumn(String column) {
        this.addColumn(column, IndexOrder.UNDEFINED);
    }

    /**
     * @param tablespace the tablespace to set
     */
    public void setTablespace(String tablespace) {
        this.tablespace = tablespace;
    }

    /**
     * {@inheritDoc}
     */
    public int compareTo(IndexElement o) {
        CompareToBuilder builder = new CompareToBuilder();
        builder.append(this.table, o.table);
        builder.append(this.name, o.name);
        builder.append(this.columns.size(), o.columns.size());
        if (builder.toComparison() == 0) {
            Iterator<Tuple<String, IndexOrder>> it = this.columns.iterator();
            Iterator<Tuple<String, IndexOrder>> oit = o.columns.iterator();
            while (it.hasNext()) {
                Tuple<String, IndexOrder> t = it.next();
                Tuple<String, IndexOrder> ot = oit.next();
                builder.append(t.head(), ot.head());
                builder.append(t.tail(), ot.tail());
            }
        }
        return builder.toComparison();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(this.table).append(this.name).append(this.columns).toHashCode();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || this.getClass() != obj.getClass()) {
            return false;
        }
        IndexElement o = (IndexElement) obj;
        return new EqualsBuilder().append(this.table, o.table).append(this.name, o.name).append(this.columns, o.columns).isEquals();
    }

    /**
     * @return nome da tabela do índice
     */
    public String getTable() {
        return this.table;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return String.format("INDEX %s para tabela %s e colunas %s", this.name, this.table, this.columns.toString());
    }

    /**
     * @return the name
     */
    public String getName() {
        return this.name;
    }

    /**
     * @return the columns
     */
    public List<Tuple<String, IndexOrder>> getColumns() {
        return this.columns;
    }

    /**
     * Verifica se o índice contempla todas as colunas dadas
     * 
     * @param someColumns someColumns
     * @return boolean
     */
    public boolean containsAllColumns(Collection<String> someColumns) {
        return this.columnSet.containsAll(someColumns);
    }
}
